import { isRef, isReactive, toRefs, watch, computed, reactive } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useQuery } from 'villus'
import { isNaN, isNil, omitBy } from 'lodash-es'
import stringify from 'fast-json-stable-stringify'

const DEFAULT_PAGE = 1
const DEFAULT_PAGE_SIZE = 10

function getPager(vars, target) {
  let {
    page,
    pageSize
  } = isRef(vars) ? vars.value : vars

  page = Number(page)
  pageSize = Number(pageSize)

  if (isNaN(page) || page < 0) {
    page = DEFAULT_PAGE
  }
  if (isNaN(pageSize) || pageSize < 0) {
    pageSize = DEFAULT_PAGE_SIZE
  }

  if (target) {
    target.page = page
    target.pageSize = pageSize
  }

  return { page, pageSize }
}

function transformVars(vars) {
  const {
    page: rawPage,
    pageSize: rawPageSize,
    where = {},
    orderBy = {},
    ...rawVars
  } = isRef(vars) ? vars.value : vars

  const { page, pageSize } = getPager(vars)

  rawVars.limit = pageSize
  rawVars.offset = (page - 1) * pageSize

  const whereKeys = Object.keys(omitBy(where, isNil))

  if (whereKeys.length) {
    rawVars.where = {}
    whereKeys.forEach(key => {
      const steps = key.replace(/\.+/g, '.').split('.')
      let s = {
        [steps.pop()]: where[key]
      }

      while (steps.length) {
        s = {
          [steps.pop()]: s
        }
      }

      Object.assign(rawVars.where, s)
    })
  }

  const orderByKeys = Object.keys(omitBy(orderBy, isNil))

  if (orderByKeys.length) {
    rawVars.orderBy = orderBy
  }

  return rawVars
}

function toWatchable(value) {
  if (isRef(value)) {
    return value
  }
  if (isReactive(value)) {
    const refs = toRefs(value)

    return Object.keys(refs).map(key => refs[key])
  }
}

function hash(x) {
  let h, i, l

  for (h = 5381 | 0, i = 0, l = x.length | 0; i < l; i++) {
    h = (h << 5) + h + x.charCodeAt(i)
  }
  return h >>> 0
}

export function usePageQuery({
  query,
  variables: rawVariables,
  ...options
}) {
  // page + pageSize => offset + limit
  const variables = transformVars(rawVariables)

  // check $_aggregate
  const paginationProps = []

  try {
    const s = query.loc.source.body

    if (typeof s === 'string') {
      s.replace(/(\S+)_aggregate/g, (str, match) => {
        paginationProps.push({
          attr: match,
          aggregate: str
        })
      })
    }
  } catch (err) {}

  // initialize useQuery
  const queryResult = useQuery({
    query,
    variables,
    ...options
  })

  // if not match _aggregate
  if (paginationProps.length === 0) {
    return queryResult
  }

  const { data, execute } = queryResult

  // use first matched aggregate
  const { attr, aggregate } = paginationProps.pop()
  const pagination = reactive({
    pageCount: 0
  })

  // overwrite data ref
  queryResult.data = computed(() => {
    const rawData = data.value || {}

    getPager(rawVariables, pagination)

    if (rawData[aggregate]) {
      const pageCount = Math.ceil(
        (rawData[aggregate]?.aggregate?.count || 0) / pagination.pageSize
      )

      // deleted last one
      if (
        pageCount >= 1 &&
        pageCount < pagination.page
      ) {
        pagination.page = pageCount
      }

      pagination.pageCount = pageCount
    }

    return {
      items: rawData[attr] || [],
      pagination: pagination.pageCount === 0 ? false : pagination
    }
  })

  // watch variables
  function startVarsWatch() {
    if (
      (!isRef(rawVariables) && !isReactive(rawVariables)) ||
      !rawVariables
    ) return
    let cache
    const watchableVars = toWatchable(rawVariables)
    const stopVarsWatch = watch(watchableVars, newVars => {
      const id = hash(stringify(newVars))

      if (id === cache) return
      cache = id
      queryResult.execute({
        variables: transformVars(rawVariables)
      })
    }, { deep: true })

    // remove watcher on pager
    onBeforeRouteLeave(stopVarsWatch)
  }

  startVarsWatch()

  return queryResult
}
